/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.alphawavesgames.engine.rendering;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

import android.opengl.GLSurfaceView.Renderer;
import android.opengl.GLU;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import javax.microedition.khronos.opengles.GL11;

/**
 *
 * @author duncan
 */
public class GLRenderHWInterface extends RenderHWInterface implements Renderer
{
    private GL10 m_gl;
    private boolean m_supportsGL11;
    private GL11 m_gl11;

    //can't use gl multithreaded so no need for the lock
    //protected final ReentrantLock m_glDeviceLock = new ReentrantLock();

    public GLRenderHWInterface()
    {
        m_gl = null;
        m_gl11 = null;
        m_supportsGL11 = false;
    }

    //lock the gl device to the callers thread ( return gl ref for use )
    public GL10 lockGL()
    {
        //Cant multithread gl so don't bother with the lock
        //m_glDeviceLock.lock();
        return m_gl;
    }

    //unlock gl device so other threads may use it
    //not valid to use the gl device you acquired in lockGL after this.
    //todo - enforce by wrapping all gl functionality in this interface
    //so we don't need to return explicit gl10 ref in lockGL.
    public void unlockGL()
    {
        //Cant multithread gl so don't bother with the lock
        //m_glDeviceLock.unlock();
    }


    /*
     * (non-Javadoc)
     *
     * @see
     * android.opengl.GLSurfaceView.Renderer#onSurfaceCreated(javax.
     * microedition.khronos.opengles.GL10, javax.microedition.khronos.
     * egl.EGLConfig)
     */

    public void onSurfaceCreated( GL10 gl, EGLConfig config )
    {
        //TODO : should we recreate all resources here?
        
        System.out.println( "OpenGLRenderer::onSurfaceCreated() - Setup gl surface" );

        //set the new gl interface to the render container
        //how do we get the screen width height?
        //or should we set to zero and re-create when screen size changes?
        create( 0, 0 );

        gl.glEnable( GL10.GL_TEXTURE_2D );					//Enable Texture Mapping

        // Set the background color to blue ( rgba ).
        gl.glClearColor( 0.0f, 1.0f, 1.0f, 0.5f );  // OpenGL docs.
        // Enable Smooth Shading, default not really needed.
        gl.glShadeModel( GL10.GL_SMOOTH );// OpenGL docs.
        // Depth buffer setup.
        gl.glClearDepthf( 1.0f );// OpenGL docs.
        // Enables depth testing.
        gl.glEnable( GL10.GL_DEPTH_TEST );// OpenGL docs.
        // The type of depth testing to do.
        gl.glDepthFunc( GL10.GL_LEQUAL );// OpenGL docs.
        // Really nice perspective calculations.
        gl.glHint( GL10.GL_PERSPECTIVE_CORRECTION_HINT, // OpenGL docs.
                GL10.GL_NICEST );

        gl.glEnable( GL10.GL_BLEND );							//Enable blending
        //gl.glDisable(GL10.GL_DEPTH_TEST);					//Disable depth test
        gl.glBlendFunc( GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA );		//Set The Blending Function For Translucency

        if( gl instanceof GL11 )
        {
            m_supportsGL11 = true;
            m_gl11 = ( GL11 )gl;
        }

        m_renderContainer.onRenderHWInterfaceCreated();
    }

    /*
     * (non-Javadoc)
     *
     * @see
     * android.opengl.GLSurfaceView.Renderer#onDrawFrame(javax.
     * microedition.khronos.opengles.GL10)
     */
    public void onDrawFrame( GL10 gl )
    {
        //set the valid gl device so other objects can get access
        m_gl = gl;
        //shouldn't need to test every time just cast it here regardless
        if( gl instanceof GL11 )
        {
            m_supportsGL11 = true;
            m_gl11 = ( GL11 )gl;
        }

        //TODO : eventually we want to move the rendercontainer.update() call 
        //out of the mutex lock so that we can update resources while running
        //the game thread. Need to implement some other thread safe strategy for this.
        //acquire lock to wait for safe point to render
        startRender();
        
        //do the render update to load any pending resources
        m_renderContainer.update();

        try
        {
            //do all the rendering
            gl.glEnable( GL10.GL_TEXTURE_2D );					//Enable Texture Mapping

            // Set the background color to blue ( rgba ).
            gl.glClearColor( 0.0f, 1.0f, 1.0f, 0.5f );  // OpenGL docs.
            // Enable Smooth Shading, default not really needed.
            gl.glShadeModel( GL10.GL_SMOOTH );// OpenGL docs.

            gl.glEnable( GL10.GL_BLEND );							//Enable blending
            //gl.glDisable(GL10.GL_DEPTH_TEST);					//Disable depth test
            gl.glBlendFunc( GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA );

            gl.glClear( GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT );

            m_renderContainer.getSpriteRenderer().render();
            
        }
        finally
        {
            endRender();
        }

        //gl device can be invalid outside the scope of this method
        m_gl = null;

    }

    /*
     * (non-Javadoc)
     *
     * @see
     * android.opengl.GLSurfaceView.Renderer#onSurfaceChanged(javax.
     * microedition.khronos.opengles.GL10, int, int)
     */
    public void onSurfaceChanged( GL10 gl, int width, int height )
    {
        if( height == 0 )
        { 						//Prevent A Divide By Zero By
            height = 1; 						//Making Height Equal One
        }

        create( width, height );
        gl.glViewport( 0, 0, width, height ); 	//Reset The Current Viewport
        gl.glMatrixMode( GL10.GL_PROJECTION ); 	//Select The Projection Matrix
        gl.glLoadIdentity(); 					//Reset The Projection Matrix

        GLU.gluOrtho2D( gl, 0, width, 0, height );
        //Calculate The Aspect Ratio Of The Window
        //GLU.gluPerspective( gl, 45.0f, ( float ) width / ( float ) height, 0.1f, 100.0f );

        gl.glMatrixMode( GL10.GL_MODELVIEW ); 	//Select The Modelview Matrix
        gl.glLoadIdentity(); 					//Reset The Modelview Matrix
    }



    //-----------------------------------------------------------------
    //----- API WRAPPER METHODS

    @Override
    public void drawIndexedTriList( int numTris, int startIndex )
    {
        m_gl11.glDrawElements( GL11.GL_TRIANGLES, numTris * 3, GL11.GL_UNSIGNED_SHORT, startIndex );
    }

    
    //-----------------------------------------------------------------
    //----- GL RESOURCE INNER CLASSES

    //creator method so we make the right type for the specific interface
    @Override
    public IndexBuffer createIndexBuffer()
    {
        return new GLIndexBuffer();
    }

    public class GLIndexBuffer extends IndexBuffer
    {
        private int m_bufferID;

        public GLIndexBuffer()
        {
            m_bufferID = 0;
        }

        @Override
        public void create( int numIndices, char[] data )
        {
            super.create( numIndices, data );

            int bufferID[] = new int[1];
            m_gl11.glGenBuffers( 1, bufferID, 0 );
            m_bufferID = bufferID[0];

            //upload data to GFX mem
            m_gl11.glBufferData(GL11.GL_ELEMENT_ARRAY_BUFFER, m_indices.capacity() * CHAR_SIZE, m_indices, GL11.GL_STATIC_DRAW );
        }

        //bind to the render device
        @Override
        public void bind()
        {
            m_gl11.glBindBuffer( GL11.GL_ELEMENT_ARRAY_BUFFER, m_bufferID );
        }

        //unbind from the render device
        @Override
        public void unbind()
        {
        }

    }


    //creator method so we make the right type for the specific interface
    @Override
    public VertexBuffer createVertexBuffer()
    {
        return new GLVertexBuffer();
    }

    //Note : this is really tailored for dynamic vertex buffers for now.
    //Todo : rename or allow for static as well.
    //Todo : consider using AOS instead of SOA or allowing both.
    public class GLVertexBuffer extends VertexBuffer
    {
        private static final int MAX_STREAMS = 8;
        private int m_bufferIDs[];
        private int m_numStreams;


        public GLVertexBuffer()
        {
            m_bufferIDs = new int[ MAX_STREAMS ];
            m_numStreams = 0;
            for( int i = 0; i < MAX_STREAMS; i++ )
            {
                m_bufferIDs[i] = 0;
            }
            //m_gl11.glGenBuffers( 1, m_bufferID, 0 );
        }

        @Override
        public void createFromElementStreams( ArrayList< ElementStreamFloat > elementStreams )
        {
            m_elementStreams = elementStreams;
            m_numStreams = elementStreams.size();

            if( m_numStreams > MAX_STREAMS )
            {
                m_numStreams = MAX_STREAMS;
            }

            m_gl11.glGenBuffers( m_numStreams, m_bufferIDs, 0 );
        }

        //go through all the element streams and upload to a VBO
        public void updateData()
        {
            for( int i = 0; i < m_elementStreams.size(); i++ )
            {
                m_gl11.glBindBuffer( GL11.GL_ARRAY_BUFFER, m_bufferIDs[ i ] );
                FloatBuffer floatBuffer = m_elementStreams.get( i ).m_buffer;
                floatBuffer.position(0);
                m_gl11.glBufferData( GL11.GL_ARRAY_BUFFER, floatBuffer.capacity(), floatBuffer, GL11.GL_DYNAMIC_DRAW );
            }
        }

        //bind to the render device
        @Override
        public void bind()
        {
            for( int i = 0; i < m_numStreams; i++ )
            {
                //how do we specify what data is in the stream?
                //need to call glVertexPointer/glTexCoordPointer etc based on stream type
                m_gl11.glBindBuffer( GL11.GL_ARRAY_BUFFER, m_bufferIDs[i] );

                //this sucks a bit - probably not very fast.
                switch( m_elementStreams.get( i ).getStreamType() )
                {
                    case POSITION:
                        m_gl11.glVertexPointer( m_elementStreams.get( i ).getNumElementsPerBlock(), GL10.GL_FLOAT,
                                                m_elementStreams.get( i ).getBlockSize(), 0 );
                        break;
                    //case NORMAL:
                    //    m_gl11.glNormalPointe( m_elementStreams.get( i ).getNumElementsPerBlock(), GL10.GL_FLOAT,
                    //                            m_elementStreams.get( i ).getBlockSize(), 0 );
                    //    break;
                    case TEXCOORD:
                        m_gl11.glTexCoordPointer( m_elementStreams.get( i ).getNumElementsPerBlock(), GL10.GL_FLOAT,
                                                  m_elementStreams.get( i ).getBlockSize(), 0 );
                        break;
                    //case COLOUR:
                    //    break;
                }
            }
        }

        //unbind from the render device
        @Override
        public void unbind()
        {
        }
    }


    public class GLTexture extends Texture
    {
        private int[] m_glTex;
        private int m_assetID;


        protected TextureGL( int assetID )
        {
            m_assetID = assetID;
            m_glTex = new int[1];
            m_glTex[0] = -1;

            m_pendingLoad = true;
        }

        @Override
        public void update()
        {
            if( m_pendingLoad )
            {
                load( m_assetID, m_context );
            }
        }

        //load the android specific drawable - doesn't really need loading
        public boolean load( int assetID, Context context )
        {
            GL10 gl = m_renderInterface.lockGL();

            boolean result = false;
            try
            {
                if( gl != null )
                {
                    InputStream is = context.getResources().openRawResource( assetID );
                    Bitmap bitmap = null;
                    try
                    {
                        //BitmapFactory is an Android graphics utility for images
                        bitmap = BitmapFactory.decodeStream(is);

                    }
                    finally
                    {
                        //Always clear and close
                        try
                        {
                            is.close();
                            is = null;
                        }
                        catch( IOException e )
                        {
                        }
                    }

                    //Generate one texture pointer...
                    gl.glGenTextures(1, m_glTex, 0);
                    //...and bind it to our array
                    gl.glBindTexture( GL10.GL_TEXTURE_2D, m_glTex[0] );

                    //Create Nearest Filtered Texture
                    gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
                    gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);

                    //Different possible texture parameters, e.g. GL10.GL_CLAMP_TO_EDGE
                    gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_REPEAT);
                    gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT);

                    //Use the Android GLUtils to specify a two-dimensional texture image from our bitmap
                    GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);

                    //Clean up
                    bitmap.recycle();

                    m_pendingLoad = false;

                    result = true;
                }
                else
                {
                    //mark as pending to be loaded so we can try again with a valid gl device
                    m_pendingLoad = true;
                    result = false;
                }
            }
            finally
            {
                m_renderInterface.unlockGL();
            }

            return result;
        }

        public int getGLTex() { return m_glTex[0]; }
    }
}
